// voter-client.c -- Client program for voter-server.
//=========================================================================
// Copyright (C) 2024 The C++ Component Model(COMO) Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//=========================================================================

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/select.h>
#include <unistd.h>
#include <termios.h>
#include <errno.h>
#include "voter_message.h"
#include "voter_lib.h"        // rudp.h included
#include "voter_client.h"

extern Rudp rudp;
extern struct sockaddr_in serv_addr; // Server network info struct

// Check command line arguments
// mode = 1: TCP, 2: UDP, 3: TCP with debug, 4: UDP with debug
int checkArguments(int argc, char **argv)
{
    int mode = 1; //默认为TCP, 无debug

    if (argc < 4) {
        printf("Usage: %s <-t/-u> <host> <port>\n", argv[0]);
        printf("\t-t\tUse TCP\n");
        printf("\t-u\tUse UDP\n");
        exit(1);
    }
    if ((argc == 5) && (argv[4][1] == 'd')) {
        mode += 2;
    }
    if (strcmp(argv[1], "-u") == 0) {
        mode += 1;
    }
    printf("mode = %d\n", mode);
    return mode;
}

/**
 * Put the terminal in raw mode: this way we will receive every
 * single key stroke as soon as the user types it. No buffering
 * nor translation of escape sequences of any kind.
 */
int SetRawMode(int fd, int enable)
{
    /**
     * We have a bit of global state (but local in scope) here.
     * This is needed to correctly set/undo raw mode.
     */
    static struct termios orig_termios; // Save original terminal status here.
    static int atexit_registered = 0;   // Avoid registering atexit() many times.
    static int rawmode_is_set = 0;      // True if raw mode was enabled.

    struct termios raw;

    /**
     * If enable is zero, we just have to disable raw mode if it is
     * currently set.
     */
    if (0 == enable) {
        // Don't even check the return value as it's too late.
        if (rawmode_is_set && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1) {
            rawmode_is_set = 0;
        }
        return 0;
    }

    // Enable raw mode.
    if (! isatty(fd)) {
        goto fatal;
    }

    if (! atexit_registered) {
        atexit(DisableRawModeAtExit);
        atexit_registered = 1;
    }

    if (tcgetattr(fd, &orig_termios) == -1) {
        goto fatal;
    }

    raw = orig_termios;  // modify the original mode

    /**
     * input modes: no break, no CR to NL, no parity check, no strip char,
     * no start/stop output control.
     */
    raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);

    /**
     * output modes - do nothing. We want post processing enabled so that
     * \n will be automatically translated to \r\n.
     */
    // raw.c_oflag &= ...
    // control modes - set 8 bit chars
    raw.c_cflag |= (CS8);

    /**
     * local modes - choing off, canonical off, no extended functions,
     * but take signal chars (^Z,^C) enabled.
     */
    raw.c_lflag &= ~(ECHO | ICANON | IEXTEN);

    /**
     * control chars - set return condition: min number of bytes and timer.
     * We want read to return every single byte, without timeout.
     */
    raw.c_cc[VMIN] = 1;
    raw.c_cc[VTIME] = 0;    // 1 byte, no timer

    // put terminal in raw mode after flushing
    if (tcsetattr(fd, TCSAFLUSH, &raw) < 0) {
        goto fatal;
    }

    rawmode_is_set = 1;
    return 0;

fatal:
    errno = ENOTTY;
    return -1;
}

/**
 * At exit we'll try to fix the terminal to the initial conditions.
 */
void DisableRawModeAtExit(void) {
    SetRawMode(STDIN_FILENO,0);
}

/**
 * Mininal line editing.
 */
void TerminalCleanCurrentLine(void)
{
    size_t num = write(fileno(stdout), "\e[2K", 4);
    if (num != 4) {
        //
    }
}

void TerminalCursorAtLineStart(void)
{
    size_t num = write(fileno(stdout), "\r", 1);
    if (num != 1) {
        //
    }
}

/**
 * Append the specified character to the buffer.
 */
int InputBufferAppend(struct InputBuffer *ib, int c)
{
    if (ib->len >= IB_MAX) {
        return IB_ERR; // No room.
    }

    ib->buf[ib->len] = c;
    ib->len++;
    return IB_OK;
}

/**
 * Process char input for an input buffer. Performs different actions based on 
 * the type of input character, such as handling newlines, backspaces, or 
 * appending characters to buffer & displaying them.
 * 
 * @param ib Point to the InputBuffer structure used to store input characters.
 * @param c  The input char, of type int, to support special char handling.
 * @return   Status code (IB_OK for single char ok, IB_GOTLINE for line ready)
 */
int InputBufferFeedChar(struct InputBuffer *ib, int c)
{
    switch (c) {
        case '\n':
            break;          // Ignored. We handle \r instead.
        case '\r':
            return IB_GOTLINE;
        case 127:           // Backspace.
            if (ib->len > 0) {
                ib->len--;
            }
            // After a backspace, update the displayed buffer content
            InputBufferHide(ib);
            InputBufferShow(ib);
            break;
        default:
            if (InputBufferAppend(ib,c) == IB_OK) {
                size_t num = write(fileno(stdout), ib->buf + ib->len-1, 1);
                if (num != 1) {
                    // write() fails
                }
            }
            break;
    }

    return IB_OK;
}

/**
 * Hide the line the user is typing.
 */
void InputBufferHide(struct InputBuffer *ib)
{
    (void)ib; // Not used var, but is conceptually part of the API.
    TerminalCleanCurrentLine();
    TerminalCursorAtLineStart();
}

/**
 * Show again the current line. Usually called after InputBufferHide().
 */
void InputBufferShow(struct InputBuffer *ib)
{
    int num = write(fileno(stdout), ib->buf, ib->len);
    if (num != ib->len) {
        //
    }
}

/**
 * Reset the buffer to be empty
 */
void InputBufferClear(struct InputBuffer *ib)
{
    ib->len = 0;
    InputBufferHide(ib);
}

// Read data from the server
void handleServerData(int s, struct InputBuffer *ib, bool *first, char *buf, size_t bufSize)
{
    ssize_t nread = read(s, buf, bufSize);
    if (nread <= 0) {
        printf("Connection lost\n");
        exit(1);
    }
    if (*first) {
        printf("Received signature from server.\n");
        //print_hex(buf, nread);
#ifdef ZMQ_MODE
        ResponseZMQ(s, true);
        printf("Responsed to server: ZMQ_MODE\n");
#else
        ResponseZMQ(s, false);
        printf("Responsed to server: NON_ZMQ_MODE\n");
#endif
        *first = false;
        return;
    }

    InputBufferHide(ib);
    
    printf("Received %ld bytes from server:\n", nread);
    print_hex(buf, nread);
    
    VoterMessage *msg = (VoterMessage *) buf;
    printVoterMessage(msg);

    InputBufferShow(ib);
}

/**
 * Read data from standard input (keyboard)
 * 
 * @param stdin_fd  stdin  fd
 * @param s         socket fd
 * @param ib        input buffer to store and process user input
 * @param buf       temporary buffer for reading user input
 * @param bufSize   size of the temporary buffer
 * @param isTCP     is TCP or UDP
 * 
 * Read user input from standard input & send messages to the remote socket
 */
void handleUserInput(int stdin_fd, int s, struct InputBuffer *ib, char *buf, size_t bufSize, bool isTCP) {
    // Read data from stdin (keyboard) to a temporary buffer
    ssize_t nread = read(stdin_fd, buf, bufSize);
    for (int j = 0; j < nread; j++) {
        // Enter each character into the input buffer
        int res = InputBufferFeedChar(ib, buf[j]);
        switch (res) {
            case IB_GOTLINE:
                // When a full line of input is detected, it is added to the buffer and hidden
                InputBufferAppend(ib, '\n');
                InputBufferHide(ib);

                // Displays prompts and user input on stdout (screen)
                int num = write(fileno(stdout), "you> ", 5);
                num += write(fileno(stdout), ib->buf, ib->len);

                // num += write(s, ib.buf, ib.len);
                // Construct VoterMessage for transmission
                char buffer[MAX_VoterMessage_SIZE];
                char type;
                uint64_t trans_id;
                int64_t data;
                char str[128]={0};
                
                // Parse input format
                // [] means a scan set; [^}] means stop when meeting '}'
                if (sscanf(ib->buf, "{%c,%lu,%ld,%[^}]}", &type, &trans_id, &data, str) != 4) {
                    fprintf(stderr, "Wrong format, {type,trans_id,data,buf} required.\n");
                    InputBufferClear(ib);
                    return;
                }

                type -= '0';   // ascii, '0'(48) --> '\0'(0)

                // Set lamportClock
                uint64_t lamportClock = 0;

                // BoxVoterMessage
                VoterMessage *box = BoxVoterMessage(buffer,
                            MAX_VoterMessage_SIZE,   // bufSize
                            type,                    // type
                            trans_id,                // idTransaction
                            lamportClock,            // lamportClock
                            data,
                            str,
                            strlen(str));

                if (NULL == box) {
                    printf("BoxVoterMessage() failed.\n");
                }

                // Send message regarding the protocol (TCP/UDP)
                if (isTCP) {
                    // Send the constructed VoterMessage to socket s
                    num += write(s, box, box->len);

                    // Here we use print_hex() as a substitute of dump()
                    printf("written %d bytes:\n", box->len);
                    print_hex((char*)box, box->len);

                    // Check if bytes written are not as expected
                    if (num != (5 + ib->len + box->len)) {
                        printf("Error: written %d bytes\n", num);
                    }
                }
                else { // UDP
                    RudpSend(&rudp, (const char*)box, box->len);
                    RudpPackage *p = RudpUpdate(&rudp, NULL, 0, 1);
                    udp_send(p, s, serv_addr);

                    printf("written %d bytes:\n", p->sz);
                    print_hex(p->buffer, p->sz);
                }
                // Show data package info
                printVoterMessage(box);

                // Clear the input buffer & prepare for the next input
                InputBufferClear(ib);
                memset(box->buf, 0, strlen(str));
                break;
            case IB_OK:
            default:
                // For incomplete input or error, do nothing
                break;
        }
    }
}

/**
 * Main program logic, finally :)
 */
int main(int argc, char **argv)
{
    // Check & parse arguments
    int mode = checkArguments(argc, argv);
    bool isTCP = (mode % 2);
    bool isDebugMode = (mode > 2);

    int s;
    if (isDebugMode){
        printf("Debug mode\n");
    }
    // Create TCP Connection
    if (isTCP){
        printf("Using TCP\n");
        // argv[2] == server address，argv[3] == port
        s = createTCPConnection(argv[2], atoi(argv[3]));
    }
    // Create UDP Connection
    else {
        printf("Using UDP\n");
        Rudp *U = RudpNew(&rudp, 1, 5);
        if (NULL == U) {
            ;
        }
        s = CreateUDPClient(atoi(argv[3]));
    }

    // Set the standard input to raw mode so that user input can be read directly
    SetRawMode(fileno(stdin), 1);

    // Init fd_set
    fd_set readfds;
    int stdin_fd = fileno(stdin);

    // Init the input buffer
    struct InputBuffer ib;
    InputBufferClear(&ib);

    // Mark whether the data is received for the first time
    bool first = true;
    // Generic buffer for both code paths
    char buf[MAX_VoterMessage_SIZE];

    // Debug mode
    if (isDebugMode){
        int type = VOTER_SYNC_CLOCK;
        while (true) {
            // Init fd_set
            FD_ZERO(&readfds);
            FD_SET(s, &readfds);

            // Use select() to listen to events in the fd_set
            int num_events = select(s+1, &readfds, NULL, NULL, NULL);
            if (-1 == num_events) {
                perror("select() error");
                exit(1);
            }
            // If there's data from server
            else if (0 != num_events && FD_ISSET(s, &readfds)) {
                handleServerData(s, &ib, &first, buf, sizeof(buf));
                // For now, the test is simple:
                // send Type 1(SYNC), Type 2(MONITOR) & Type 3(START_NEW),
                // since voting requires multiple processes (clients)
                sendVoterMessage(s, type, 0, 0, 0, NULL, 0, isTCP);
                type++;
                if (type == VOTER_CMD_VOTING_DATA){
                    exit(0);
                }
            }
        }
    }
    
    // The main loop, listens for keyboards and network events
    while (true) {
        // Init fd_set
        FD_ZERO(&readfds);
        FD_SET(s, &readfds);
        FD_SET(stdin_fd, &readfds);
        // Determine max fd
        int maxfd = s > stdin_fd ? s : stdin_fd;

        // Use select() to listen to events in the fd_set
        int num_events = select(maxfd+1, &readfds, NULL, NULL, NULL);
        if (-1 == num_events) {
            perror("select() error");
            exit(1);
        }
        // If an event occurs, determine which fd has the event
        else if (0 != num_events) {
            // Data from the server
            if (FD_ISSET(s, &readfds)) {
                handleServerData(s, &ib, &first, buf, sizeof(buf));
            }
            // Data from the user typing on the terminal
            else if (FD_ISSET(stdin_fd, &readfds)) {
                handleUserInput(stdin_fd, s, &ib, buf, sizeof(buf), isTCP);
            }
        }
    }
    close(s);

    return 0;
}
